GraalVM + Micronautでリフレクションを使いたい

GraalVM + Micronautでリフレクションを使いたい

Clock Icon2022.11.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Introduction

GraalVMは、多言語対応の仮想マシンとプラットフォームです。
Java/JVM言語/Node/LLVM言語をサポートしており、
Javaプログラムをネイティブコンパイルして高速動作が可能です。
このへん参照

GraalVMではAOT(Ahead Of Tim)コンパイルと呼ばれるコンパイルができます。
これはJavaのコードをNative Imageと呼ばれる
実行可能形式にコンパイルできるのですが、
リフレクションをつかっている場合にそのままだと
実行できないことがあります。
本稿ではMicronaut + GraalVMで
リフレクションを使いたいケースについて試してみました。

Environment

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 12.4

Setup

MicronautやGraalVMのインストールにはSdkManを使います。
インストールしてない場合はインストールしましょう。

% curl -s "https://get.sdkman.io" | bash

GraalVMとMicronautをインストールします。

% sdk update
% sdk install java 22.3.r17-grl
% sdk install micronaut

・・・

% java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

% mn --version
Micronaut Version: 3.7.3

インストールOKです。
次にアプリを実装していきます。   

Try

local_grという名前でGraalVM用アプリの雛形を作成します。

% mn create-app example.local_gr \
    --features=graalvm,serialization-jackson \
    --build=gradle --lang=java

次に、src/main/exampleにリフレクションを使って呼び出すクラスを作成します。
シンプルなメソッドを1つ持っているだけのクラスです。

package example;

public class ReflectionService {
    private void sayHello() {
        System.out.println("hello");
    }
}

src/main/exampleにHelloController.javaを作成。
ここでReflectionServiceのメソッドをリフレクションで呼び出します。

package example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;

@Controller("/hello") 
public class HelloController {

    @Get("/reflection") 
    public String sayHello() {

        ReflectionService service = new ReflectionService();

        Class<? extends ReflectionService> clazz = service.getClass();
        try {
            Method printHoge = clazz.getDeclaredMethod("sayHello");
            printHoge.setAccessible(true);
            printHoge.invoke(service, (Object[])null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }

        return "sayHello";
    }
}

ネイティブコンパイルします。
3〜4分くらいかかる。

% ./gradlew nativeCompile

コンパイルできたらネイティブイメージを起動しましょう。

% ./build/native/nativeCompile/local_gr
 __  __ _                                  _
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
  Micronaut (v3.7.3)

16:07:17.267 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 355ms. Server Running: http://localhost:8080

アクセスしてみます。
これは問題なく動作します。

% curl http://localhost:8080/hello/reflection

この場合はリフレクションを使っていても、コンパイル時の静的解析で検出可能だからです。
しかし、ControllerのsayHelloメソッドを次のようにすると、
クエリパラメータによって呼び出される関数が
動的に変化するため実行できません。

    @Get("/reflection") 
    public String sayHello(Optional<String> name) {

        ReflectionService service = new ReflectionService();

        Class<? extends ReflectionService> clazz = service.getClass();
        try {
            Method printHoge = clazz.getDeclaredMethod(name.orElseThrow());
            printHoge.setAccessible(true);
            printHoge.invoke(service, (Object[])null);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }

        return "sayHello";
    }

スタックトレースをみると、NoSuchMethodExceptionが発生してエラーになっています。

% curl http://localhost:8080/hello/reflection?name=sayHello
{"message":"Internal Server Error","_links":{"self":[{"href":"/hello/reflection?name=sayHello","templated":false}]},"_embedded":{"errors":[{"message":"Internal Server Error: null"}]}}

reflection-config.jsonで設定

動的に呼び出すクラス・メソッドが変化する場合、
設定ファイルで事前にどういった定義が必要が記述しておきます。

src/main/resources/META-INF/native-image/example/local_grディレクトリに
reflection-config.jsonを下記のように作成します。

[ {
    "name" : "example.ReflectionService",
    "methods" : [ {
      "name" : "sayHello",
      "parameterTypes" : [ ]
    }]
  }
]

そして、build.gradleに次の定義を追加します。

graalvmNative {
    binaries {
        main {
            buildArgs.add("-H:ReflectionConfigurationFiles=/path/your/local_gr/src/main/resources/META-INF/native-image/example/local_gr/reflection-config.json")
        }
    }
}

ネイティブコンパイル時の引数に
さきほどのreflection-config.jsonを指定してあげます。

再度ネイティブコンパイルして起動。
今度はちゃんとメソッドが呼び出しできています。

% ./gradlew nativeCompile
% ./build/native/nativeCompile/local_gr

#今度は動く
% curl http://localhost:8080/hello/reflection?name=sayHello

Tracing Agentでreflection-configの自動生成

今回の例のように単純なものであればいいのですが、
リフレクションを多用している場合にいちいち
設定ファイルを書いていくのは現実的ではないです。

そういった場合、Tracing Agentというツールを使って
設定ファイルの自動生成を行います。

このツールは、VMの実行時(ネイティブイメージでない状態でアプリ実行しているとき)に
アプリの動作をトレースすることにより、JSONファイルを生成するツールです。
具体的には、単体テスト実行時にその挙動をトレースして定義ファイルを生成します。

では、build.gradleでテスト実行時にTracing Agentを使うように指定します。

test {
    jvmArgs "-agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/auto/"
}

元からあるテストクラス(test/java/example/Local_grTest.java)に、
ReflectionServiceのメソッドを実行するテストを追加します。

    @Test
    void reflectTest() {
        Class<? extends ReflectionService> clazz = ReflectionService.class;
        try {
            ReflectionService service = clazz.newInstance();
            Method printHoge = clazz.getDeclaredMethod("sayHello");
            printHoge.setAccessible(true);
            printHoge.invoke(service, (Object[])null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

テストを実行。

./gradlew test

テストが終わると、build.gradleで指定したパスに
reflection-config.jsonなどの設定ファイルもろもろが生成されてます。


・・・

{
  "name":"example.ReflectionService",
  "methods":[
    {"name":"<init>","parameterTypes":[] }, 
    {"name":"sayHello","parameterTypes":[] }
  ]
},

・・・

そしてネイティブコンパイル・起動すれば、生成したファイル郡がロードされて、
先程とおなじく動きます。
※build.gradleのgraalvmNativeセクションは削除してOK

Summary

今回はGraalVM + Micronautでリフレクションの動作を確認してみました。
GraalVMは今後Javaでパフォーマンスを求めるなら避けては通れないと思っているので、
しっかりおさえておきたいところです。

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.